Technotes
QuickDraw GX ConicLibrary.c in Detail: Description and Derivations
|
Important for all Apple Printing and Graphics Developers:
The information in this Technote is still relevant up to and including Mac OS 7.6 with QuickDraw GX 1.1.5. Beginning with the release of Mac OS 8.0, however, Apple plans to deliver a system which incorporates QuickDraw GX graphics and typography only. QuickDraw GX printer drivers and GX printing extensions will not be supported in Mac OS 8.0 or in future Mac OS releases. Apple's goal is to simplify the user experience of printing by unifying the Macintosh graphic and printing architectures and standardizing on the classic Printing Manager. For details on Apple's official announcement, refer to </technotes/gxchange.html> |
This Technote discusses ConicLibrary.c from the QuickDraw GX Libraries.
This Note is intended for Macintosh QuickDraw GX developers who want to
approximate ellipses and hyperbolas with paths. These, with parabolas
(gxCurves), form the familty of curves called conics.
About the GX LibrariesFor better or worse, the development of QuickDraw GX took seven years from conception to initial release. During that time, there were many requests for feature enhancements and interface improvements that, if implemented, might have taken seven more years to complete. As it turns out, some of these enhancements could readily be built on existing services, but there was no time to test or document these services with the rigor required to make them fully part of the released system.The GX Libraries fill this gap by providing services built on top of the rest of QuickDraw GX in source form. This Technote and others document these services. Since GX libraries are provided as source, it is reasonable for developers to modify them to meet their specific needs. Care was taken for the libraries not to depend on the implementation details of QuickDraw GX so that future versions should not invalidate them, in original or modified form. The libraries are likely to evolve to take advantage of improved algorithms, as well as new Macintosh or QuickDraw GX services. If you modify one for your application's specific needs, it's worth occasionally reviewing the GX library provided by Apple to stay synchronized with any improvements.
What is in ConicLibrary.c?The GX Library, ConicLibrary.c, generates conics, a family of curves that include circles, ellipses, parabolas and hyperbolas. It was written by Michael Reed (the guy behind GX Fonts and TrueType) from a math paper derived by Robert Johnson, the GX resident mathematics professor.Using ConicLibrary.cA Conic is a gxCurve, AlmostConicLibrary.c uses the conic struct:
struct conic { gxPoint a; gxPoint b; gxPoint c; Fixed lambda; };This is nearly the same as the gxCurve struct; it differs only by the additional lambda parameter. (Mike was a math major, too.) The lamba specifies the curvature. If the curvature is equal to one, the conic is a parabola, and is identical to a gxCurve (also known as a quadratic Bézier) with the same three points, as illustrated in Figure 1. In Figure 1, as lambda decreases, the curve approaches the line from point a to point c. All of these curves are elliptical arcs. When lambda hits zero, the curve has become a line from point a to point c. As lambda increases, the curve approaches point b. These curves are hyperbolas. Once lambda gets big enough, the curve approximates two line segments: one from point a to point b, and another from point b to point c.
Figure 1: A Quadratic Rational Spline with Vertices (a,b,c) One reasonable use of the ConicLibrary provides a graphics application with a means to change the amount of curvature defined by a quadratic Bézier, a segment of a gxPath. To increase the curvature, increase lambda.
Conic FunctionsBefore we get into what the various values for lambda do, let's take a look at the conic library functions.
NewConicgxShape NewConic(const conic *curve);NewConic creates a path which approximates the conic. It subdivides the original conic into a pair of conics, recursively, until the gxCurve described by the smaller conic has a sufficiently small error. The error is set by the default path's shape error, which is initially 1.0. You can set it to a larger or smaller error with:
GXSetShapeCurveError(GXGetDefaultShape(gxPathType), errorValue); DrawConicvoid DrawConic(const conic *curve, gxShapeFill fill);DrawConic creates the path shape, draws it with the specified fill, and throws it away. The fill can be any gxShapeFill value that works with a path; gxOpenFrameFill is the most common choice.
SetConicvoid SetConic(gxShape target, const conic *curve);This replaces the geometry in an existing shape with a path that approximates the conic.
The Theory behind ConicLibrary.cIn the discussion that follows, several liberties are taken. Since the reader is assumed to be a programmer, not a mathematician, most of the equations that follow are in C++ rather than in mathematical notation. The non-existent structs fPoint, fCurve and fConic are introduced, to make the code easier to read. They are defined as:
struct fPoint { float x; float y; }; struct fCurve { fPoint first; fPoint control; fPoint last; }; struct fConic { fPoint a; fPoint b; fPoint c; float lambda; };
circle: x2 + y2 = 0 parabola: x = y2 hyperbola: x2 - y2 = 1 ellipse: x2/a + y2/b = 0We're going to mostly ignore these, except to note that they can all be generalized by:
conic: ax2 + bx + cy2 + dy + exy + f = 0This looks suspicously like the equation describing a parabola (with the right values of a, b, c, d, e and f) ≠ a gxCurve; and it is. Letπs see how.
Describing a CurveThe general conic equation asserts that any equation of order 2 in x and y describes a conic. A conic, in turn, is described as a plane section of a pair of cones, stacked point to point. Slicing these cones reveals the familiar circles, ellipses, parabolas and hyperbolas.There's another way to describe a parabola: as a parametric equation. A parametric equation (at least as they are going to be described here) describes x and y in terms of two equations as a third parameter, t, varies from zero to one. The equation for a second order parametric equation is:
x = at2 + bt + cIt's easy to see how this describes at least a non-rotated parabola; setting a or d to zero allows specifying an xy equation where either x or y is squared. More importantly for computers, it's easy to turn this equation into a forward differencing algorithm that allows approximating the curve as a series of lines with computations no more complicated than adds and shifts. If the curve described by these equations is limited by t starting at zero and ending at one, then the curve begins at (c, f) and ends at (a + b + c, d + e + f). Plugging in 0.5 for t yields the curve's midpoint; i.e., x = a/4 + b/2 + c. It is convenient to be able to define a curve by where it starts and ends; all that's missing is some additional information that describes how it curves. A gxCurve describes a parabolic segment in terms of three control points, named first, control and last. We can describe those points in terms of their parametric parameters:
first = cThe control point is formed by intersecting the tangent lines at the start and end of the curve. We can express the mid point in terms of first, control and last by:
mid = a/4 + b/2 + c = c/4 + b/4 + c/2+ (a + b + c)/4 = first/4 +control/2 + last/4Solving for a, b and c results in:
a = first - 2 * control + lastBy substituting the above equations, we can specify a pair of parametric equations that describe a curve, given three points: (C++ code begins here:)
void CurveXY(const fCurve& cu, float& x, float& y, const float t) { x = (cu.first.x - 2 * cu.control.x + cu.last.x) * t * t + 2 * (cu.control.x - cu.first.x) * t + cu.first.x; y = (cu.first.y - 2 * cu.control.y + cu.last.y) * t * t + 2 * (cu.control.y - cu.first.y) * t + cu.first.y; }We can regroup the terms to reference the points' ordinates once:
void CurveXY(const fCurve& cu, float& x, float& y, const float t) { x = cu.first.x * ( 1 - t) * (1 - t) + 2 * cu.control.x * t * (1 - t) + cu.last.x * t * t; y = cu.first.y * ( 1 - t) * (1 - t) + 2 * cu.control.y * t * (1 - t) + cu.last.y * t * t; }Thus, the mid point of the curve is computed to be:
if ( t == 0.5) { // optimize ! x = cu.first.x/4 + cu.control.x/2 + cu.last.x/4; y = cu.first.y/4 + cu.control.y/2 + cu.last.y/4; }Perspective geometry tells us that putting the points that describe a gxCurve or a gxPath through a gxMapping is accurate only if the gxMapping is affine; that is, it contains scaling, skewing, rotation and translation, but no perpsective. If a gxMapping is a perspective transformation, then a parabola becomes either a hyperbolic or elliptical segment. If any conic is sent through a perspective transformation, the result is another conic. In Technote 1051 - Understanding Conic Splines, we learn that the parametric form for conics can be represented by sending the parametric equation for a quadratric Bézier through a perspective transformation:
void FindConicXY(const fConic& con, float& x, float& y, const float t) { x = (con.a.x * ( 1 - t) * (1 - t) + 2 * con.lambda * con.b.x * t * (1 - t) + con.c.x * t * t) / (1 + 2 * (con.lambda -1) * t * (1 - t)); y = (con.a.y * ( 1 - t) * (1 - t) + 2 * lambda * con.b.y * t * (1 - t) + con.c.y * t * t) / (1 + 2 * (con.lambda -1) * t * (1 - t)); }Note that if we plug in 1.0 for con.lambda, the denominator becomes equal to 1.0; the numerator becomes identical to the CurveXY function derived above. The mid-point for any conic simplifies to:
if ( t == 0.5) { // optimize ! x = (con.a.x/2 + con.lambda * con.b.x + con.c.x/2) / (con.lambda + 1); y = (con.a.y/2 + con.lambda * con.b.y + con.c.y/2) / (con.lambda + 1); }If lambda equals 1, then this becomes:
if (t == 0.5 && con.lambda == 1) { // further optimize ! x = con.a.x/4 + con.b.x/2 + con.c.x/4; y = con.a.y/4 + con.b.y/2 + con.c.y/4; }which is identical to the equation for a gxCurve or parabola that we derived above. In summary, what do we know?
Computing LambdaNow let's determine the appropriate lambda value for a circular arc. Here's the decidedy GX-centric approach we'll take:a) Construct a pair of lines perpendicular to the tangents at the arc's ends. b) Define the circle's center to be the intersection of these lines. c) Rotate one of these lines to the mid-angle to define the arc's mid-point. d) Use the arc's mid-point to define the arc's lambda value. So you can try this on your own,we'll code this as legitimate fixed-point code.
#includeFor this example to work, the arc b point must be equidistant from the a and c points. For example:
conic arc = {{0, 0}, {ff(125), ff(0)}, {ff(200), ff(100)}}; FindCircularLambda(arc); DrawConic(&arc); SummaryGX Libraries have a wealth of information and show how to use QuickDraw GX to solve real problems. The Conic Library shows how to use GX to construct paths that approximate conics, given three control points and a weight.
Reference MaterialThere are several different directions that conics can be taken. It's possible that a future version of GX will directly support conics in all of its primitive operations. Until then, it is straight-forward to take the information presented here, forget all of it, and read Technote 1051 - Understanding Conic Splines instead to derive the code to transform conics through general gxMappings.Another possible line of development is to adopt the general xy form equation for conics to use with GX. Foley and Van Damm's venerable Computer Graphics (2nd edition) from Addison-Wesley presents code for scan converting conics in their general form. This algorithm is further explored on the Internet at http://www.ece.uiuc.edu/~ece291/class-resources/gpe/conic.cc.html.
There are also a number of Internet resources to explore conics further.
Listing them here will likely produce references that quickly grow out of date,
but you might try linking to http://www.geom.umn.edu/apps/conics/ or
http://www.bham.ac.uk/mathwise/syl3_2.htm.
|
Previous Technote | Contents | Next Technote |